#include "SimulinkLinkInstance.hpp"

#include "cybertron/core/UtilPath.hpp"
#include "cybertron/core/Log.hpp"

#include "Util/UtilString.hpp"
#include "Util/Platform.hpp"

#include <Windows.h>
#include <future>

typedef std::basic_streambuf<char16_t> StreamBuffer;

typedef size_t(*ENGINE_FIND_SHARED_MATLAB)(char16_t*** names);
typedef void(*ENGINE_CREATE_SESSION)();
typedef void(*ENGINE_EVAL_WITH_COMPLETION)(const uint64_t matlabHandle, const char16_t* statement, void(*success)(void*), void(*exception)(void*, size_t, const void*), void* p, void* output, void* error, void(*write)(void*, const char16_t*, size_t), void(*deleter)(void*), uintptr_t** handles);
typedef uint64_t(*ENGINE_ATTACH_SHARED_MATLAB)(const char16_t* name, bool* errFlag);
typedef void(*UTIL_UTF8_TO_UTF16)(const char* utf8, char16_t** utf16, size_t* errType);
typedef uint64_t(*ENGINE_CREAT_OUT_OF_PROCESS_MATLAB)(char16_t** options, size_t size, bool* errFlag);

UTIL_UTF8_TO_UTF16 util_utf8_to_utf16;

ENGINE_EVAL_WITH_COMPLETION engine_eval_with_completion;
ENGINE_CREATE_SESSION engine_create_session;
ENGINE_FIND_SHARED_MATLAB engine_find_shared_matlab;
ENGINE_ATTACH_SHARED_MATLAB engine_attach_shared_matlab;
ENGINE_CREAT_OUT_OF_PROCESS_MATLAB engine_create_out_of_process_matlab;

bool SimulinkLinkInstance::init()
{
    //get simonemodule path 
    std::string exePath = cybertron::UtilPath::getExecutablePath();

    auto matlabBasePaths = getMatlabPaths();
    if (!matlabBasePaths.empty()) {
        std::string matlabPath = determinMatlabVersion(matlabBasePaths);
        logInfo("Use matlab path: %s", matlabPath.data());
        if (matlabPath.empty()) {
            logInfo("==No Matlab path found");
            return false;
        }

        std::vector<std::string> matlabBasePathSplit;
        matlabBasePathSplit = cybertron::UtilString::split(matlabPath, "\\\\");
        logInfo("==Matlab base path: %s", matlabBasePaths.back().data());
        int matlabVersion = std::atoi(matlabBasePathSplit.back().c_str());
        logInfo("==Matlab Version %d", matlabVersion);
        // get c++ dll
        std::string dllPath = matlabPath + "\\extern\\bin\\win64\\libMatlabEngine.dll";
        size_t sizePath = dllPath.length();
        wchar_t *buffer = new wchar_t[sizePath + 1];
        MultiByteToWideChar(CP_ACP, 0, dllPath.c_str(), sizePath, buffer, sizePath * sizeof(wchar_t));
        buffer[sizePath] = 0;
        HINSTANCE hMatlabDll = LoadLibrary((LPCWSTR)buffer);
        delete buffer;
        buffer = nullptr;
        logInfo("Load Matlab Library...");
        util_utf8_to_utf16 = (UTIL_UTF8_TO_UTF16)GetProcAddress(hMatlabDll, "util_utf8_to_utf16");
        if (matlabVersion > 2018) {
            engine_create_session = (ENGINE_CREATE_SESSION)GetProcAddress(hMatlabDll, "cpp_engine_create_session");
            engine_find_shared_matlab = (ENGINE_FIND_SHARED_MATLAB)GetProcAddress(hMatlabDll, "cpp_engine_find_shared_matlab");
            engine_eval_with_completion = (ENGINE_EVAL_WITH_COMPLETION)GetProcAddress(hMatlabDll, "cpp_engine_eval_with_completion");
            engine_attach_shared_matlab = (ENGINE_ATTACH_SHARED_MATLAB)GetProcAddress(hMatlabDll, "cpp_engine_attach_shared_matlab");
            engine_create_out_of_process_matlab = (ENGINE_CREAT_OUT_OF_PROCESS_MATLAB)GetProcAddress(hMatlabDll, "cpp_engine_create_out_of_process_matlab");
        }
        else {
            engine_create_session = (ENGINE_CREATE_SESSION)GetProcAddress(hMatlabDll, "engine_create_session");
            engine_find_shared_matlab = (ENGINE_FIND_SHARED_MATLAB)GetProcAddress(hMatlabDll, "engine_find_shared_matlab");
            engine_eval_with_completion = (ENGINE_EVAL_WITH_COMPLETION)GetProcAddress(hMatlabDll, "engine_eval_with_completion");
            engine_attach_shared_matlab = (ENGINE_ATTACH_SHARED_MATLAB)GetProcAddress(hMatlabDll, "engine_attach_shared_matlab");
            engine_create_out_of_process_matlab = (ENGINE_CREAT_OUT_OF_PROCESS_MATLAB)GetProcAddress(hMatlabDll, "engine_create_out_of_process_matlab");
        }
        if (!engine_create_session) {
            logInfo("Fail to get proc address");
            return false;
        }
        //find matlab
        logInfo("Matlab: createsession");
        engine_create_session();
        char16_t** names;
        size_t size = engine_find_shared_matlab(&names);
        logInfo("Matlab: find shared matlab");
        if (size == 0) {
            logInfo("No shared Matlab found, please run 'matlab.engine.shareEngine' in Matlab");
            return false;
        }
        logInfo("Matlab: shared matlab found");

        std::vector<std::u16string> names_v(size);
        for (size_t i = 0; i < size; i++) {
            logInfo("Matlab shared name: %s", names[i]);
            names_v[i] = names[i];
        }
        names = nullptr;
        //connect matlab
        engine_create_session();
        bool errFlag = false;
        logInfo("Matlab: engine attach to shared matlab");
        mMatlabHandle = engine_attach_shared_matlab(names_v[0].c_str(), &errFlag);
        if (errFlag) {
            logInfo("MATLAB session cannot be connected.");
            return false;
        }
        // command
        matlabEval("openslx=split(gcs,'/');");
        matlabEval("slxname=char(openslx(1));");
    }

    mIsValid = true;
    return true;
}

std::string SimulinkLinkInstance::determinMatlabVersion(const std::vector<std::string>& matlabPaths)
{
    for (auto& path : matlabPaths) {
        logInfo("DeterminMatlabVersion for path: %s", path.data());
        if (findProcess(path) > 0) {
            return path;
        }
    }
    return "";
}

void SimulinkLinkInstance::startSim()
{
    if (mIsValid) matlabEval("set_param(slxname,'SimulationCommand','start')");
}

void SimulinkLinkInstance::pauseSim()
{
    if (mIsValid) matlabEval("set_param(slxname,'SimulationCommand','pause')");
}

void SimulinkLinkInstance::continueSim()
{
    if (mIsValid) matlabEval("set_param(slxname,'SimulationCommand','continue')");
}

void SimulinkLinkInstance::endSim()
{
    if (mIsValid) matlabEval("set_param(slxname,'SimulationCommand','stop')");
}



inline void set_eval_promise_data(void *p) {
    std::promise<void>* prom = reinterpret_cast<std::promise<void>*>(p);
    prom->set_value();
    delete prom;
}
inline void set_eval_promise_exception(void *p, size_t excTypeNumber, const void* msg) {
    //set_promise_exception<void>(p, excTypeNumber, msg);
}
inline void writeToStreamBuffer(void* buffer, const char16_t* stream, size_t n) {
    std::shared_ptr<StreamBuffer>* output = reinterpret_cast<std::shared_ptr<StreamBuffer>*>(buffer);
    output->get()->sputn(stream, n);
}
inline void deleteStreamBufferImpl(void* impl) {
    delete static_cast<std::shared_ptr<StreamBuffer>*>(impl);
}

void SimulinkLinkInstance::matlabEval(std::string str) {

    void* output_ = nullptr;
    void* error_ = nullptr;
    std::promise<void>* p = new std::promise<void>();
    uintptr_t* handles;
    std::vector<uintptr_t> handlesV(3);
    char16_t* name = nullptr;
    size_t errType = 0;
    util_utf8_to_utf16(str.c_str(), &name, &errType);;
    std::u16string statement = name;
    engine_eval_with_completion(mMatlabHandle, statement.c_str(), &set_eval_promise_data, &set_eval_promise_exception, p, output_, error_, &writeToStreamBuffer, &deleteStreamBufferImpl, &handles);
    handles = nullptr;
    name = nullptr;
    output_ = nullptr;
    error_ = nullptr;
}